10. 函数

函数部分内容比较多,也比较杂,为了避免同学们觉得枯燥,我们把它分成了两部分,第二部分放入高级语法部分,这样 对于一些同学掌握第一部分就可以了。

函数是代码的一种组织形式。

我们从开始到现在的代码都是一句一句的,从上到下执行而来,但这种结构在 实际工作中肯定是不行的,我们不能写一万行代码都从头撸到尾,这个时候比较合理的 做法是把我们的任务按照一定的规则进行分类,分解成小的任务,小的任务或许还要继续分解,这样一个小的任务有一段代码来完成,这段代码相对比较独立,我们把这种 代码的组织形式可以叫做函数。

所以呢,函数就是完成一定任务的一段代码,除了完成一定功能,还有一个重要的好处就是我们可以重复使用这段代码。 关于重复使用,以前我们的做的是把代码 复制粘贴一遍,但有了函数,我们可以给完成某个任务的代码起个名字,然后每次需要这个功能的时候直接用这个名字代替就好,即所谓的函数调用。

把任务分解成函数的原则有两个比较主要的:

  • 高内聚:通俗的讲就是一个函数完成一个单一功能,而且只完成一个聚焦的单一功能

  • 低耦合:可以理解成是函数的代码或者功能跟外部的联系或者依赖比较少,这样的代码复用性比较大,后期维护成本低

函数的使用规则一般要这两步:

  • 函数需要先定义

  • 使用函数,俗称调用

函数的顶一个格式大概如下:

 def func_name(para1, para2, ... ):
    func_body

10.1. 一个函数的定义小栗子

下面代码展示了如何定义一个函数,此处的关键字 def 需要顶格写,

# 定义一个函数
# 只是定义的话不会执行
# 1. def关键字,后跟一个空格
# 2. 函数名,自己定义,起名需要遵循便令命名规则,约定俗成,大驼峰命名只给类用
# 3. 后面括号和冒号不能省,括号内可以由参数
# 4. 函数内所有代码缩进

def func():
    print("我是一个函数")
    print("我要完成一定功能")
print("我结束了")

此处函数还没有被调用,我们只是定义了一个函数,一旦函数定义成功,他就会静静的呆着,知道它被调用才开始起作用。

运行上面代码的结果是:

    # 函数只是被定义,并没有被调用
    # 所以没有运行
    我结束了

因为函数已经被定义好了,那么我们就可以让他去干活了,只要调用一下就可以,每调用一次他就执行一次,所以,函数属于模块化编程的 内容,最大的好处之一就是可以被重复使用,简称复用:

调用我们定义的函数代码如下:

# 函数的调用
# 直接函数名后面跟括号
# 调动函数必须后面有括号,至于括号后面写啥,需要看情况
func()

函数被调动后,运行结果如下:

我是一个函数
我要完成一定功能

10.2. 函数的参数

在完成某一个具体任务前,往往有很多前提条件,比如需要一定数据才能正常工作,那么这种情况我们可以通过称之为 参数的功能实现。

参数是负责给函数传递一些必要的数据或者信息的变量,在定义的时候需要在定义函数第一行的小括号内写出来,Python 直接写出变量的名称就好,不需要指定变量的类型,两个问题:

  • 命名规则: 参看变量命名规则,要求望文意,跟前面变量没有任何关系

  • 变量位置: 多个参数摆放位置有一定规则,同类型参数位置可调

参数分类根据不同的分法有不同的分类,这里先介绍一种分法,即按照参数的使用场景分为两类:

  • 形参(形式参数): 在函数定义的时候用到的参数没有具体值,只是一个占位的符号,成为形参

  • 实参(实际参数): 在调用函数的时候输入的值

下面案例定义了一个参数,变量名称是person,这里通过名称我们猜测是一个人,那么对于整个函数来说, 他用到这个变量的时候都拿person这个变量做为例子使用,这个就是式形式,具体person的值是多少,其实 是不知道的, 所以,在定义的时候我们给的参数是形参。

# 参数的定义和使用
# 参数person只是一个符号,代表的是调用的时候的某一个数据
# 调用的时候,会用p的值代替函数中所有的person

def hello(person):
    print("{0}, 你肿么咧".format(person))
    print("Sir, 你不理额额就走咧")

下面代码我们调用上面定义的函数,此时,我们需要给参数传入一个值,在执行过程中这个值是被用来替代person的,我们只要在 定义person的位置用一个实际的值代替即可,此时的参数是一个实际的值,我们成为实参。

p = "明月"

# 调用函数hello的时候,p代替了person,p是一个具体的参数,简称实参
hello(p)

代码运行结果如下:

    明月, 你肿么咧
    Sir, 你不理额额就走咧

10.3. 函数的返回值

执行一个任务总得有个结果,我们的函数有时候需要把结果返回给调用者,所谓件件有着落事事有结果,本小姐研究下返回结果的问题。

返回结果即函数的调用执行结果,这个结果如果我们需要,可以把他直接赋值给一个变量,如下所示:

    # 执行函数后,结果会赋值给变量result
    result = call_a_funciton()

函数中如果需要返回,我们需要用到关键字return, 关键字后面跟需要返回的结果。

10.3.1. 返回一个值

很多语言里函数可以不返还结果,有的语言甚至把返回结果的和不返回结果的函数用名称做出了区分,Python比较特殊,如果没有返回,则系统会默认给返回一个None,所以,任何Python’函数调用都可以把调用值赋值给变量,跟上面调用的格式一样。

这里关键字return一但被执行,则函数调用会被无条件终止,直接返回结果,后面无论什么代码都不会被执行。

下面函数返回一个填充完毕的字符串,我们可以调用函数并把执行结果返回给一个变量:

    # return语句的基本使用
    # 函数打完招呼后返回一句话
    def hello(person):
        print("{0}, 你肿么咧".format(person))
        print("Sir, 你不理额额就走咧")

        return "我已经跟{0}打招呼了,{1}不理我".format(person, person)

    p = "明月"
    
    # 调用函数,使用参数p, 并把调用结果赋值给rst
    rst = hello(p)
    
    print(rst)

上面案例执行结果如下:

明月, 你肿么咧
Sir, 你不理额额就走咧
我已经跟明月打招呼了,明月不理我

关于函数返回值的终止执行功能,我们看下面的案例:

# return案例2
def hello(person):
    print("{0}, 你肿么咧".format(person))
    return "哈哈,我提前结束了"
    print("Sir, 你不理额额就走咧")
    return "我已经跟{0}打招呼了,{1}不理我".format(person, person)

p = "刘大拿"
rst = hello(p)
print(rst)

执行案例结果如下,我们看到,在第一个return后面的两个语句没有执行,所以只能得到下面的结果:

    刘大拿, 你肿么咧
    哈哈,我提前结束了

10.3.2. 返回多个值

有同学可能比较疑惑,函数如果返回多个值怎么办? 或者如果我需要函数返回多个值的时候,what should I do?

这个其实很简单,两句话的事儿:

  1. Python只允许返回一个值

  2. 如果你返回多个值,函数自动把多个值当成一个叫tuple类型的数据,且要求返回的时候多个值用逗号分开

多个结果自动打包成tuple类型返回,tuple后面会讲

古人日, 说不清楚的事就吃个栗子吧, 安排:

   # 定义一个函数
   def more_return(curse):
        # 函数返回三个内容,分别是两个字符串和一个数字
        # 中间用逗号隔开
        return '有情人', 100, '眷属'

   # 调用函数,返回结果给变量rst
   rst = more_return("我爱王晓静")
   print('返回结果的类型是:', type(rst))
   print('返回结果是:', rst) 

上面代码执行结果如下,可以显示出函数返回结果的类型是tuple, 返回结果可以打印出来:

    返回结果的类型是: <class 'tuple'>
    返回结果是: ('有情人', 100, '眷属')

10.4. 函数帮助文档

Python自带电池,面对这么多的电池,我们到底如何选择? 这个时候帮助文档就显得尤其重要,如果我们不明白某一个函数的功能,Python给我们提供了 一个可以快速查看对方文档的函数-help

10.4.1. 帮助文档的查看

help是系统内置函数,我们对任何不明白的功能,可以知己使用help(function_name)来查看别的函数的文档,例如下面,我们对系统提供的打印功能print不太 明白,则可以直接help(print)来查看print函数的功能,但这里需要注意的是,print函数只需要把名字写出来就好,不需要也不能跟这个后面的小括号( )

运行help(print)后的结果如下所示,为了帮助大家看懂帮助文档,这里中文的内容是我添加的注释:

# 提示这个是关于什么的帮助文档
Help on built-in function print in module builtins:

# 提示函数的命名
print(...)
    # 这个是函数的定义,包括各种参数的位置,大致用法,类别,此知识点后面会讲
    # 这个就是所谓函数的签名
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

    # 一句话说明函数的功能
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    
    # 对函数用到的各个参数做出解释
    # 现在还流行另外一种类Java的说明,但大致都差不太多
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

好了,通过上面的查看,我们应该能了解个关于print函数的七七八八,基本够用了,这其中包含了功能介绍,参数的说明,甚至个别难理解的函数或者不常用的 会有使用小示例。

10.4.2. 帮助文档的编写

人们越来越把软件开发看做一个工程而不是个人行为,这里要求程序员要有完备的文档,作为后期维护,交接甚至自己修改的有力助手,否则很可能在某个时候 你会发现一段很土鳖的代码,在你边骂边理解的折腾半天后才豁然开朗,这就是当年你自己写的。

函数文档的编写,在函数定义的的第一行使用三引号直接定义就可以,使用help对帮助文档的查看也是调用的这一部分内容。

请看下面函数的文档编写:

def func_doc(name):
'''
只是一个测试函数,用来测试函数的文档
param name: 文档的参数,用来表示文档的名字
'''
pass

help(func_doc)

上面案例定义一个函数后。编写好帮助文档,然后利用help显示出了函数文档的内容,显示结果如下

Help on function func_doc in module __main__:

func_doc(name)
    只是一个测试函数,用来测试函数的文档
    param name: 文档的参数,用来表示文档的名字

10.5. 细说参数

我们上面学习过,所谓函数的参数就是在函数执行的时候给函数提供一些必要的数据,具体参数从是否有具体的值角度可以分为实参和形参,从另一个维度 上讲,别入位置,形式等,参数还可以分为以下四种:

  • 普通参数

  • 默认参数

  • 关键字参数

  • 收集参数

下面我们分别对着四种参数进行介绍,希望大家听完后能豁然开朗蓦然回首长叹一声怅然若失百折不挠…

10.5.1. 普通参数/位置参数(PositionalArgument)

在函数定义中,常见的是普通参数,此类参数在函数定义中只是一变量名称,具体需要注意的是:

  • 可以有多个普通参数,中间用逗号隔开就好

  • 参数顺序不重要,调用的时候根据位置传入的值和参数一一对应即可,注意,位置是唯一对应的依据,参考英文名称(PositionalArg)

  • 普通参数要放在所有其他类函数前面

普通函数的定义格式如下所示:

        def 函数名(参数名,参数名...):
            函数中的代码(python语句)
            函数中的代码(python语句)
            ...
        
        调用函数: 函数名(值,值...)

来一个简单的栗子吧:

    def intro(name, age, school, slogan):
        info = f'''My name is {name}, i am {age} years old,
                I 毕业于{school}, our slogan is {slogan}'''
        print(info)

    intro('刘大拿', 19, "北京图灵学院",'我爱王晓静')

上面案例执行结果如下:

    My name is 刘大拿, i am 19 years old,
                I 毕业于北京图灵学院, our slogan is 我爱王晓静

如果我们在调用的时候,把参数的顺序故意传错,会出现下面的情况,但是,Python不会因此向你报告错误,也就是说,Python根本不检查参数的含义和变量类型。 例如我们调用形式如下;

intro(19, "北京图灵学院",'我爱王晓静', '刘大拿')

如上面调用,则得到如下结果:

    My name is 19, i am 北京图灵学院 years old,
                        I 毕业于我爱王晓静, our slogan is 刘大拿

结果陷入驴唇不对马嘴了,这里我想说的是,这种调用仅限于参数个数相同的时候,即调用参数和定义的时候参数个数相同,如果不相同,会报错的。

10.5.2. 默认参数

如果采用默认参数,我们在调用的时候不但一定要给出相应个数的参数,而且参数顺序也不能改变,但在实际时候用当中,还有两外一种情况,比如 我们要求我们学校的所有学生都做一个自我介绍,问题是,学生肯定都是我们学校的,这个时候我们如果每次调用都输入一遍北京图灵学院作为学校 名称,显得很傻,能不能给一个默认的值,如果我们不对这个参数赋值,就用默认的值,如果我们不想用默认的值,就直接给出,这样就把选择权交给了 调用者本身。

此类参数叫做默认参数,即在调用的时候如果没有给出相应的值,则采用默认的值。

默认参数的格式大概如下:

        def 函数名(形参名 = 默认值,形参名 = 默认值...):
            函数中的代码(python语句)
            函数中的代码(python语句)
            ..

        
        调用函数1, 可以不给出参数,此时参数的值就是默认的值:
            函数名()

        调用函数2,如果给出相应参数,则给出的值会替代默认的值:
            函数名(实参,实参...)

下面栗子中,默认参数school放在了参数列表最后,定义或者调用的时候有个规则,即形式参数必须放在位置参数后面,否则报错:

     def intro(name, age, school='北京图灵学院'):
        print(name, age, school)

函数调用的时候,可能会有多种方式,请大家仔细看调用代码:

    # 利用位置调用参数
    intro('刘大拿', 19)

    # 前两个参数传给name和age
    intro('刘大拿', age=19, school='北京大学')

    # 下面调用会得到一个错误
    # 错误提示:positional argument follows keyword argument
    # intro(age=19, '刘大拿', school='北京大学')   

运行结果如下:

    刘大拿 19 北京图灵学院
    刘大拿 19 北京大学            

10.5.3. 关键字参数

在传入参数的时候,我们有时候防止顺序出现问题,可以用参数名称=参数值的方式调用函数,参数这样用称为关键字参数。

关键字参数用于调用函数的时候 默认参数用于定义函数

关键词参数消除了参数调用时候的参数传入顺序问题,个人比较喜欢,省的因为顺序问题导致错误。

关键字参数的调用形式为:

        调用函数:
            函数名(形参名 = 实参值,形参名 = 实参值...)

需要注意的是,利用关键字参数调用,关键字参数必须统一放在普通参数后面,否则出错

关键字参数的案例跟默认参数案例相同, 编程如果很多参数,除了前面几个比较常用或者习惯用法外,一般后面 的参数都为默认参数或者调用的时候用关键字参数调用,防止混淆:

    def intro(name, age, school='北京图灵学院'):
        print(name, age, school)
        
    # 利用位置调用参数
    intro('刘大拿', 19)

    # 前两个参数传给name和age
    intro('刘大拿', age=19, school='北京大学')

    # 下面调用会得到一个错误
    # 错误提示:positional argument follows keyword argument
    # intro(age=19, '刘大拿', school='北京大学')                      

10.5.4. 收集参数/可变参数(*args

Python有一个很聪明的方法,如果不确定参数调用的时候会输入多少个参数,或者允许调用者输入随意个参数的时候, 这个时候可以定义一个收集参数,也叫可变参数,在调用的时候,如果函数定义只定义了一个普通参数,这个时候如果调用的时候 传入了多个普通参数,此时除了第一个,其余所有的否放入了一个特定的变量里,收集参数或者可变参数的名称由此得来。

收集参数分为两种,分别用来收集位置参数和关键词字参数:

  • *args: 收集默认/位置参数

  • **kargs: 收集关键字参数

默认参数收集参数大概格式如下,此处需要注意点是星号是必须的,后面的变量名称args只是约定俗成,一般不用更改名称:

        def 函数名(*args):
            函数中的代码(python语句)
            函数中的代码(python语句)
            ...

        调用函数: 函数名(实参,实参....)

Achtung:

  1. 使用收集参数进行形参的书写,需要在形参名之前添加*

  2. *形参格式的收集参数会收集到调用函数时传入的所有没有关键字的形参

  3. 收集参数收集到的最终数据是由所有非关键字实参组成的元组(元祖后面讲)。

  4. 收集参数和普通的参数(关键字参数)可以共存

请看下面代码:

    def intro(name, *args):
        
        print('Args类型:', type(args))
        
        # 高端的代码往往采用简单的烹饪方式,后面会讲
        print(name, *args)
        
        # 或者也可以这样, 后面会讲
        print(name)
        for i in args:
            print(i)
        
    # 调用,后面两个参数会放入args内
    intro('刘大拿', 19, '北京图灵学院')

上面代码中, 函数调用的时候使用了三个位置参数,但因为函数声明只有一个位置参数,所以后面两个自动打包装入了收集参数args中, 在函数 中如果需要,可以直接从args按顺序取出使用。

上面案例为了展示args代码的使用,尽量多写了两行,同时还把args的类型打印出来了,可以看到它是hightuple类型的数据,下面为运行结果:

    Args类型: <class 'tuple'>
    刘大拿 19 北京图灵学院
    刘大拿
    19
    北京图灵学院

10.5.5. 关键字参数收集参数(**kargs)

除了位置参数收集参数外,还有一个专门用于收集关键字参数的收集参数,定义格式如下所示:

        def 函数名(**kargs):
            函数中的代码(python语句)
            函数中的代码(python语句)
            ...

        调用函数:函数名(形参名 = 值,形参名=值...)

Attention:

  1. 使用收集参数收集关键字实参的方法需要在形参名之前加**

  2. **形参格式会收集调用函数时传入函数的所有关键字参数

  3. 收集的所有关键字参数最终组成的数据为字典类型

  4. 关键字参数收集的方式可以和普通形参共存,但是必须在最后

请看案例:

    def intro(name, *args, **kargs):
        print('kargs类型:', type(kargs))
        # 高端的代码往往采用简单的烹饪方式,后面会讲
        for k,v in kargs.items():
            print(k, v)
        
    # 调用,后面两个关键字参数会放入kargs内
    # `\`表示此处换行,当一行代码太长的时候推荐换行,长度为一屏宽为最大限度
    intro('刘大拿', 19, '北京图灵学院', \
          love='王晓静', slogan='Ich liebe Wangxiaojing')

上面代码我们在掉好用的时候前三个参数为位置参数,此时第一个参数赋值给那么, 剩下两个放入手机参数args中作为元祖保存,后面的关键字 参数loveslogan作为关键字参数放入收集参数kargs内保存,函数内部显示收集参数的类型,同时把收集参数内容打印出来, 当然字典类型的使用我们还没有讲。

运行结果如下:

    kargs类型: <class 'dict'>
    love 王晓静
    slogan Ich liebe Wangxiaojin

10.5.6. 函数参数最后的啰嗦

函数参数比较繁琐,最后的陈词总结如下:

  • 函数定义时参数要有一定顺序:

    1. 最先定义默认参数/位置参数

    2. 其次是默认参数

    3. 其次是位置参数收集参数

    4. 最后是关键字参数收集参数

  • 同类型参数顺序可以颠倒

  • 调用的时候收集参数只接收前面没有匹配上的参数

  • 可以把元祖/列表或者字典作为参数直接传给函数,但是需要星号(*)配合

请看下面一个关于解包的例子,这个例子我们以后学了元祖和字典后会理解,这里先了解下:

    def intro(name, *args, **kargs):
        
        print(name)
        print(*args)

        for k,v in kargs.items():
            print(k, v)
        
    # 定义了一个列表
    pa = ['刘大拿', 19, '北京图灵学院']  

    # 定义了一个字典
    ka = {'love':'王晓静', 'slogan':'Ich liebe Wangxiaojing'} 
        
    # 对列表和字典解包后作为参数调用函数
    intro(*pa, **ka)

运行结果如下:

    刘大拿
    19 北京图灵学院
    love 王晓静
    slogan Ich liebe Wangxiaojing

10.6. 函数的一个小栗子

这节课理论多点,加个例子吧,相关案例在习题课里介绍的很多,大家可以去学习,谢谢。

# 九九乘法表
# version 1.0
for row in range(1,10):
    # 打印一行
    for col in range(1, row+1):
        # print函数默认任务打印完毕后换行
        print( row * col, end=" ")
    print("-------------------")

运行结果如下:

1 -------------------
2 4 -------------------
3 6 9 -------------------
4 8 12 16 -------------------
5 10 15 20 25 -------------------
6 12 18 24 30 36 -------------------
7 14 21 28 35 42 49 -------------------
8 16 24 32 40 48 56 64 -------------------
9 18 27 36 45 54 63 72 81 -------------------

把打印九九乘法表中的一行做成一个函数试试:

# 定义一个函数,打印一行九九乘法表
def printLine(row):
    for col in range(1, row+1):
        # print函数默认任务打印完毕后换行
        print( row * col, end=" ")
    print("")

# 九九乘法表
# version 2.0
for row in range(1,10):
    printLine(row)

运行结果如下:

    1
    2 4
    3 6 9
    4 8 12 16
    5 10 15 20 25
    6 12 18 24 30 36
    7 14 21 28 35 42 49
    8 16 24 32 40 48 56 64
    9 18 27 36 45 54 63 72 81